#ifndef __EXPRESSION__HPP__
#define __EXPRESSION__HPP__

#include "Globals.hpp"

using namespace std;

// Forward declarations to declare traits used in BinaryExpression for checking
// types.
struct Add;
struct Ceil;
struct Floor;
struct Min;
struct Max;

template < class E>
struct Expression {
  E expr_;
  constexpr Expression (E e) : expr_ (e) {}

  // Just forward down to either BinaryExpression, either VariableTerm or ConstantTerm
  template <typename ... T>
  constexpr int Val(std::tuple<T ...> vals) {
    return expr_.Val(vals);
  }
  
  template <typename ... T>
  constexpr int Min(std::tuple<T ...> const &mins, std::tuple<T ...> const &maxes) {
    return expr_.Min(mins, maxes);
  }
  
  template <typename ... T>
  constexpr int Max(std::tuple<T ...> const &mins, std::tuple<T ...> const &maxes) {
    return expr_.Max(mins, maxes);
  }

  constexpr Expression operator-() {
    return -expr_;
  }
};

// Global variables for printing and debugging purposes
template <class E> ostream & operator<<(ostream &os, const Expression<E> &e) {
  expr_depth++;
  os << std::string(expr_depth, '\t') << "Expr of depth " << expr_depth
     << ": " << endl;
  os << e.expr_;
  os << std::string(expr_depth, '\t') << "END Expr" << endl;
  expr_depth--;
  return os;
}

template < class L, class H, class OP>
struct BinaryExpression {
  L l_;
  H h_;
  constexpr BinaryExpression (L l, H h) : l_ (l), h_ (h) {}

  constexpr BinaryExpression operator-() {
    return BinaryExpression(-l_, -h_);
  }

  // We essentially have two type of operations:
  // 1. evaluations where we plug in a set of values and want the output value
  // of the expressions
  // 2. ranges where we plug in a min and a max set of values and want a min
  // or a max result for the expression

  // Should we also have a bool output here depending on the type of OP ? 
  // Can we just use 0 / non-0 output ?
  template <typename ... T>
  constexpr int Val(std::tuple<T ...> const &vals) {
    return OP::apply (l_.Val(vals), h_.Val(vals));
  }

  template <typename ... T>
  constexpr int Min(std::tuple<T ...> const &mins, std::tuple<T ...> const &maxes) {
    static_assert(is_computation_op<OP>::value, 
                  "EXPRESSION CONTAINS A NON-COMPUTATION OP, CANNOT CALL MIN ON IT");
    return OP::apply (l_.Min(mins, maxes), h_.Min(mins, maxes));
  }

  template <typename ... T>
  constexpr int Max(std::tuple<T ...> const &mins, std::tuple<T ...> const &maxes) {
    static_assert(is_computation_op<OP>::value, 
                  "EXPRESSION CONTAINS A NON-COMPUTATION OP, CANNOT CALL MAX ON IT");
    return OP::apply (l_.Max(mins, maxes), h_.Max(mins, maxes));
  }

};

template <class L, class H, class OP> 
ostream & operator<<(ostream &os, const BinaryExpression<L, H, OP> &e) {
  bin_expr_depth++;

  os << std::string(bin_expr_depth, '\t') 
     << "BinExpr (" << OP::name() << ") of depth " 
     << bin_expr_depth << ": " << endl;
  os << std::string(bin_expr_depth, '\t') << "L:" << endl << e.l_;
  os << std::string(bin_expr_depth, '\t') << "H:" << endl << e.h_;
  os << std::string(bin_expr_depth, '\t') << "End BinExpr: " << endl;

  bin_expr_depth--;

  return os;
}


// template < class E, int PLACE>
// struct LowerBound {
//   VariableTerm<PLACE> t_;
//   Expression<E> expr_;

//   constexpr LowerBound (VariableTerm<PLACE> t, Expression<E> e) : t_(t), expr_ (e) {}
// };


// template < class E, int PLACE>
// struct UpperBound {
//   VariableTerm<PLACE> t_;
//   Expression<E> expr_;

//   constexpr UpperBound (VariableTerm<PLACE> t, Expression<E> e) : t_(t), expr_ (e) {}
// };


#endif //__EXPRESSION__HPP__
